Imports System.drawing

Namespace Overlay
    ''' <summary>
    ''' The jChart.Overlay.PieOverlay object is used to create pie charts. 
    ''' The series should be jChart.PieSeries objects and 
    ''' each series will represent a single slice of the pie chart. 
    ''' No Y Axis or X Axis values are used for this type of overlay.
    ''' </summary>
    ''' <remarks>Multiple PieOverlay objects can be layered if there is a need to display several pie charts simultaneously.</remarks>
    Public Class PieOverlay
        ''' <summary>
        ''' Indicates the positioning of displayed values within each pie slice.
        ''' </summary>
        ''' <remarks></remarks>
        Public Enum Placement As Integer
            OuterEdge = 1
            Middle = 2
        End Enum

        ''' <summary>
        ''' Indicates the value that will be displayed on a pie slice.
        ''' </summary>
        ''' <remarks></remarks>
        Public Enum Mode As Integer
            ShowValue = 1
            ShowPercent = 2
            ShowName = 3
        End Enum

        Private miCenterX As Integer = 0
        Private miCenterY As Integer = 0
        Private miInnerRadius As Integer = 0
        Private miOuterRadius As Integer

        Protected moChartDimensions As ChartDimensions
        Protected moSeries As New ArrayList

        Protected miValuePlacement As Placement = Placement.Middle
        Protected miValueMode As Mode = Mode.ShowValue
        Protected moValueFont As Font = New Font(System.Drawing.FontFamily.GenericSansSerif, 10)
        Protected moValueColor As Color = Color.Black
        Protected msValueFormat As String = ""

        Protected moRegionList As ArrayList

        ''' <summary>
        ''' oChartDimensions is a jChart.ChartDimensions object that describes the interior of the chart.
        ''' </summary>
        ''' <param name="oChartDimensions"></param>
        ''' <remarks></remarks>
        Public Sub New( _
            ByVal oChartDimensions As ChartDimensions)

            moChartDimensions = oChartDimensions

            moRegionList = New ArrayList

            If moChartDimensions.InnerHeight < moChartDimensions.InnerWidth Then
                miOuterRadius = moChartDimensions.InnerHeight / 2
            Else
                miOuterRadius = moChartDimensions.InnerWidth / 2
            End If

            miCenterX = moChartDimensions.InnerLeft + (moChartDimensions.InnerWidth / 2)
            miCenterY = moChartDimensions.InnerTop + (moChartDimensions.InnerHeight / 2)

        End Sub

        ''' <summary>
        ''' An ArrayList containing series. Series added to a PieOverlay object must be jChart.PieSeries objects.
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Property Series() As ArrayList
            Get
                Return moSeries
            End Get
            Set(ByVal value As ArrayList)
                moSeries = value
            End Set
        End Property

        ''' <summary>
        ''' A PieOverlay.Placement value indicating where value labels should be displayed in relation to the slices of the pie.
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Property ValuePlacement() As Placement
            Get
                Return miValuePlacement
            End Get
            Set(ByVal value As Placement)
                miValuePlacement = value
            End Set
        End Property

        ''' <summary>
        ''' A PieOverlay.Mode value indicating whether the value labels should contain the series name, 
        ''' the percent of the total pie represented by each slice, or the series value.
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Property ValueMode() As Mode
            Get
                Return miValueMode
            End Get
            Set(ByVal value As Mode)
                miValueMode = value
            End Set
        End Property

        ''' <summary>
        ''' Indicates the font that should be used for value labels.
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Property ValueFont() As Font
            Get
                Return moValueFont
            End Get
            Set(ByVal value As Font)
                moValueFont = value
            End Set
        End Property

        ''' <summary>
        ''' Indicates the color that should be used for value labels.
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Property ValueColor() As Color
            Get
                Return moValueColor
            End Get
            Set(ByVal value As Color)
                moValueColor = value
            End Set
        End Property

        ''' <summary>
        ''' 
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks>
        ''' http://blogs.msdn.com/kathykam/archive/2006/03/29/564426.aspx
        ''' </remarks>
        Public Property ValueFormat() As String
            Get
                Return msValueFormat
            End Get
            Set(ByVal value As String)
                msValueFormat = value
            End Set
        End Property

        ''' <summary>
        ''' The X location of the center of the pie chart. By default this is the center of the chart area.
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Property CenterX() As Integer
            Get
                Return miCenterX
            End Get
            Set(ByVal value As Integer)
                miCenterX = value
            End Set
        End Property

        ''' <summary>
        ''' The Y location of the center of the pie chart. By default this is the center of the chart area.
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Property CenterY() As Integer
            Get
                Return miCenterY
            End Get
            Set(ByVal value As Integer)
                miCenterY = value
            End Set
        End Property

        ''' <summary>
        ''' The outside radius of the pie. By default this is the largest that will fit within the chart area.
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Property OuterRadius() As Integer
            Get
                Return miInnerRadius
            End Get
            Set(ByVal value As Integer)
                miOuterRadius = value
            End Set
        End Property

        ''' <summary>
        ''' The inside radius of the pie. Changing this property to a value less than the OuterRadius 
        ''' will result in the chart having the appearance of a ring instead of a pie.
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Property InnerRadius() As Integer
            Get
                Return miInnerRadius
            End Get
            Set(ByVal value As Integer)
                miInnerRadius = value
            End Set
        End Property

        ''' <summary>
        ''' Draws all charts belonging to the OverLay on a System.Drawing.Graphics object.
        ''' </summary>
        ''' <param name="g"></param>
        ''' <remarks></remarks>
        Public Sub DrawChart(ByRef g As Graphics)
            DrawAllSeries(g)
        End Sub

        Protected Sub DrawAllSeries(ByRef g As Graphics)
            '---Count series
            Dim iAngleCount As Integer = 360 / moSeries.Count

            Dim iInitialAngle As Integer = iAngleCount

            SeriesToPie( _
                g, _
                moSeries, _
                miCenterX, _
                miCenterY, _
                miInnerRadius, _
                miOuterRadius, _
                iInitialAngle)
        End Sub

        ''' <summary>
        ''' An ArrayList containing jChart.RegionArea objects for each point described by any series belonging to the OverLay object.
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Property RegionList() As ArrayList
            Get
                Return moRegionList
            End Get
            Set(ByVal value As ArrayList)
                moRegionList = value
            End Set
        End Property

        Private Sub SeriesToPie( _
                    ByRef g As Graphics, _
                    ByVal oSeries As ArrayList, _
                    ByVal iXCenter As Integer, _
                    ByVal iYCenter As Integer, _
                    ByVal iInnerRadius As Integer, _
                    ByVal iOuterRadius As Integer, _
                    ByVal iInitialAngle As Integer)

            '---Define area values.
            Dim snSumOfSeries As Single = 0
            For Each oSlice As Series.PieSeries In oSeries
                snSumOfSeries = snSumOfSeries + oSlice.Value
            Next

            For iValue As Integer = 0 To oSeries.Count - 1
                Dim oSlice As Series.PieSeries = oSeries(iValue)
                oSlice.Percent = oSlice.Value / snSumOfSeries
            Next

            Dim iCurrentAngle As Integer

            '---First show slices without offsets
            iCurrentAngle = iInitialAngle
            For iValue As Integer = 0 To oSeries.Count - 1
                Dim oSlice As Series.PieSeries = oSeries(iValue)
                Dim iSweepAngle As Integer = 360 * oSlice.Percent

                If oSlice.Offset = 0 AndAlso _
                    oSlice.ShowSlice = True Then

                    AddPieWedge( _
                            g, _
                            oSlice, _
                            iXCenter, _
                            iYCenter, _
                            iInnerRadius + oSlice.Offset, _
                            iOuterRadius + oSlice.Offset, _
                            iCurrentAngle, _
                            iSweepAngle)
                End If

                iCurrentAngle = iCurrentAngle + iSweepAngle
            Next

            '---Next show slices with offsets
            iCurrentAngle = iInitialAngle
            For iValue As Integer = 0 To oSeries.Count - 1
                Dim oSlice As Series.PieSeries = oSeries(iValue)
                Dim iSweepAngle As Integer = 360 * oSlice.Percent

                If oSlice.Offset <> 0 AndAlso _
                    oSlice.ShowSlice = True Then

                    AddPieWedge( _
                            g, _
                            oSlice, _
                            iXCenter, _
                            iYCenter, _
                            iInnerRadius + oSlice.Offset, _
                            iOuterRadius + oSlice.Offset, _
                            iCurrentAngle, _
                            iSweepAngle)
                End If

                iCurrentAngle = iCurrentAngle + iSweepAngle
            Next

            '---Finally, draw labels
            iCurrentAngle = iInitialAngle
            For iValue As Integer = 0 To oSeries.Count - 1
                Dim oSlice As Series.PieSeries = oSeries(iValue)
                Dim iSweepAngle As Integer = 360 * oSlice.Percent

                If oSlice.ShowValue = True AndAlso _
                     oSlice.ShowSlice = True Then

                    ShowWedgeValue( _
                        g, _
                        oSlice, _
                        iXCenter, _
                        iYCenter, _
                        iInnerRadius + oSlice.Offset, _
                        iOuterRadius + oSlice.Offset, _
                        iCurrentAngle, _
                        iSweepAngle)
                End If

                iCurrentAngle = iCurrentAngle + iSweepAngle
            Next
        End Sub

        Private Sub AddPieWedge( _
                            ByVal g As Graphics, _
                            ByVal oSeries As Series.PieSeries, _
                            ByVal iX As Integer, _
                            ByVal iY As Integer, _
                            ByVal iInnerRadius As Integer, _
                            ByVal iOuterRadius As Integer, _
                            ByVal iStartingAngle As Integer, _
                            ByVal iSweepAngle As Integer)

            '---Compute array of points that represents area of wedge
            Dim iAngle As Integer = iStartingAngle - 1
            Dim iPointCount As Integer = iSweepAngle + 2

            Dim oPoints(iPointCount * 2) As Point

            For iCurPoint As Integer = 0 To iPointCount
                oPoints(iCurPoint) = FindPoint(iX, iY, iOuterRadius, iAngle)
                oPoints((iPointCount * 2) - iCurPoint) = FindPoint(iX, iY, iInnerRadius, iAngle)

                iAngle = iAngle + 1
            Next

            g.FillPolygon(New SolidBrush(Color.FromArgb(oSeries.AlphaValue, oSeries.Color.R, oSeries.Color.G, oSeries.Color.B)), oPoints)

            Dim oRegionArea As New RegionArea(oSeries.Name, 0, oSeries.Value, oPoints)
            moRegionList.Add(oRegionArea)
        End Sub

        Private Sub ShowWedgeValue( _
                        ByVal g As Graphics, _
                        ByVal oSeries As Series.PieSeries, _
                        ByVal iX As Integer, _
                        ByVal iY As Integer, _
                        ByVal iInnerRadius As Integer, _
                        ByVal iOuterRadius As Integer, _
                        ByVal iStartingAngle As Integer, _
                        ByVal iSweepAngle As Integer)

            '---Center of wedge
            Dim iAngle As Integer = iStartingAngle + (iSweepAngle / 2)
            If iAngle > 360 Then iAngle = iAngle - 360

            Dim sValue As String = ""
            Select Case miValueMode
                Case Mode.ShowName
                    sValue = oSeries.Name

                Case Mode.ShowPercent
                    sValue = FormatPercent(oSeries.Percent, 2)

                Case Mode.ShowValue
                    If msValueFormat > "" Then
                        If IsNumeric(oSeries.Value) Then
                            sValue = String.Format(msValueFormat, Val(oSeries.Value))
                        Else
                            sValue = String.Format(msValueFormat, Val(oSeries.Value))
                        End If
                    Else
                        sValue = oSeries.Value
                    End If
            End Select

            Dim oSize As SizeF = g.MeasureString(sValue, moValueFont, New Point(0, 0), StringFormat.GenericTypographic)
            Dim oMidpoint As Point = Nothing

            Select Case miValuePlacement
                Case Placement.Middle
                    oMidpoint = FindPoint(iX, iY, InnerRadius + (iOuterRadius - InnerRadius) / 2, iAngle)

                    g.DrawString( _
                        sValue, _
                        moValueFont, _
                        New SolidBrush(moValueColor), _
                        oMidpoint.X - oSize.Width / 2, _
                        oMidpoint.Y - oSize.Height / 2)

                Case Placement.OuterEdge
                    oMidpoint = FindPoint(iX, iY, iOuterRadius, iAngle)

                    If (iAngle >= 180 And iAngle < 270) Then
                        '---Upper left quadrent
                        oMidpoint.X = oMidpoint.X - oSize.Width
                        oMidpoint.Y = oMidpoint.Y - oSize.Height / 2

                    ElseIf (iAngle >= 90 And iAngle < 180) Then
                        '---Lower left quadrent
                        oMidpoint.X = oMidpoint.X - oSize.Width
                        oMidpoint.Y = oMidpoint.Y + oSize.Height / 2

                    ElseIf (iAngle >= 0 And iAngle < 90) Then
                        '---Lower right quadrent
                        oMidpoint.X = oMidpoint.X + oSize.Width
                        oMidpoint.Y = oMidpoint.Y + oSize.Height / 2

                    ElseIf (iAngle >= 270 And iAngle < 360) Then
                        '---Upper right quadrent
                        oMidpoint.X = oMidpoint.X + oSize.Width
                        oMidpoint.Y = oMidpoint.Y - oSize.Height / 2

                    End If

                    g.DrawString( _
                        sValue, _
                        moValueFont, _
                        New SolidBrush(Color.Black), _
                        oMidpoint.X - oSize.Width / 2, _
                        oMidpoint.Y - oSize.Height / 2)
            End Select

        End Sub

        Private Function FindPoint( _
                        ByVal xcenter As Long, _
                        ByVal ycenter As Long, _
                        ByVal radius As Long, _
                        ByVal angle As Long) As Point

            Dim oPoint As Point
            Dim iRadians = AngleToRadian(angle)

            oPoint.X = xcenter + (radius * Math.Cos(iRadians))
            oPoint.Y = ycenter + (radius * Math.Sin(iRadians))

            Return oPoint
        End Function

        Private Function AngleToRadian( _
            ByVal iAngle As Integer) As Double

            Dim dAngleToRadian As Double = iAngle * 2.0 * Math.PI / 360.0

            Return dAngleToRadian
        End Function
    End Class
End Namespace